feat(vault): route PAM gated ciphers through the SDK (partial-cipher pivot) - #22171
feat(vault): route PAM gated ciphers through the SDK (partial-cipher pivot)#22171Hinton wants to merge 1 commit into
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## pam/cipher-partial-decrypt #22171 +/- ##
=============================================================
Coverage ? 52.74%
=============================================================
Files ? 4207
Lines ? 133252
Branches ? 20977
=============================================================
Hits ? 70284
Misses ? 57829
Partials ? 5139 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
6771120 to
a0c1558
Compare
fc9198d to
2dfba20
Compare
2dfba20 to
76b9e53
Compare
🤖 Bitwarden Claude Code ReviewOverall Assessment: APPROVE Reviewed the partial-cipher pivot: the Code Review Details
Note: as the description states, this stays blocked on the |
| return this.showCipherView && !this.isTrashFilter && !this.showRestore; | ||
| // Hide Edit for a partial-data cipher — the host banner owns the access flow until | ||
| // the full cipher is revealed. | ||
| return this.showCipherView && !this.isTrashFilter && !this.showRestore && !this.isPartialData; |
There was a problem hiding this comment.
❓ QUESTION: Should Delete/Archive also be hidden for a partial cipher in this dialog?
Details
showEdit now excludes partials, but the footer's other mutating actions don't:
showActionButtons→this.cipher !== null && mode !== "clone", so the trash button renders and is enabled whenevercanDelete(i.e.cipher.permissions?.delete) is true.showArchiveOptions→!isAdminConsoleAction && mode === "view", so archive/unarchive can render too.
The row menu in vault-cipher-row.component.html deliberately hides delete, restore, archive, unarchive, and favorite for isPartial, and both bulk paths filter partials out — so clicking a gated row's name opens the one surface that still offers those actions.
If gated rows should expose no modify action at all, gating showActionButtons (or canDelete / showArchiveOptions) on isPartialData would make the dialog consistent with the row and bulk paths.
| /** | ||
| * Client-only, transient companion to {@link partial}: set on a full cipher served under an | ||
| * active PAM lease (full data, so `partial` is false). Never sent by the server, persisted, | ||
| * or serialized — its producer (the leased-cipher fetcher) stamps it directly on the view. | ||
| * Lets gating surfaces keep rendering lease state once a lease lands. | ||
| */ | ||
| leaseGated?: boolean; |
There was a problem hiding this comment.
♻️ DEBT: leaseGated has no producer or consumer in this repo — consider landing it with the lease fetcher.
Details
The only reference outside this declaration is cipher-view.component.spec.ts:220, which sets it but never asserts on it. Nothing reads it, so the field is inert until the "leased-cipher fetcher" it documents exists. Adding it to a core model now means every consumer of CipherView sees a field they can't act on.
Also, the doc says it is never "persisted, or serialized": toJSON() returns this, so a stamped leaseGated is written to the DECRYPTED_CIPHERS state blob — it just doesn't survive fromJSON. Worth rewording if the field stays.
5060ea7 to
b49db25
Compare
…pivot)
Route PAM-gated ("partial") ciphers through the SDK and surface them only in
the web vault, where they render read-only with a "Controlled access" badge. A
partial cipher ships a reduced `partialData` envelope in place of its full
payload; the SDK decrypts it into a view marked `partial`. Everywhere outside
the web vault list, partials are excluded — they never reach autofill, export,
reports, Fido2, key rotation, or the CLI, and are never modifiable.
Model plumbing: `CipherResponse.partialData` flows verbatim through `CipherData`
to domain `Cipher.partialData`, round-tripping losslessly through
`toSdkCipher`/`fromSdkCipher`.
Excluded-by-default streams: `getAllDecrypted()` and `cipherViews$` /
`cipherListViews$` exclude partials, derived from private partials-inclusive
sources so decryption stays single-pass. The web list consumes the opt-in
`cipherListViewsWithPartials$`. `CipherViewLikeUtils.isPartial` centralizes the
flag read.
Read-only rendering + badge seam: a gated cipher opens read-only in the
vault-item dialog and cipher-view; a host-provided "Controlled access" badge
column (`VAULT_ROW_LEASE_BADGE`) appears only when the `Pam` flag is on, a
`usePam` org is in view, and a host provides the badge.
Non-modifiable in web: gated rows are non-selectable and expose no modify menu
actions; both bulk-action paths filter them defensively; encrypted export
excludes them. The web filter tree includes partials so a type/folder present
only as gated rows still surfaces.
Inert until a PAM provider binds the badge seam and the server emits
`partialData`; with PAM off, every added filter is a no-op.
b49db25 to
3dd58dd
Compare
🎟️ Tracking
Strategy pivot for PAM cipher gating. Supersedes #22168, #22169, #22170 (now closed). Depends on bitwarden/sdk-internal#1359.
📔 Objective
Route PAM-gated ("partial") ciphers through the SDK and surface them only in the web vault, where they render read-only with a "Controlled access" badge. A partial cipher is a row whose sensitive fields the server suppressed (the caller lacks a privileged-access lease); it ships a reduced
partialDataenvelope (encrypted name + login URIs only) in place of the full payload, and the SDK decrypts it into a view markedpartial.Everywhere outside the web vault list, partials are excluded — they must never reach autofill, export, reports, Fido2, key rotation, or the CLI, and must never be modifiable (editing/re-encrypting a gated row would clobber the server-suppressed fields with blanks).
The security-critical field allowlist (a gated view exposes only name + login URIs, never password/TOTP) lives in the SDK (
RestrictedCipherDatain #1359), not in this repo.What changed
Model plumbing.
CipherResponse.partialData(optional) flows verbatim →CipherData→ domainCipher.partialData, round-tripping losslessly throughtoSdkCipher/fromSdkCipher. The SDK decrypts gated rows like any other cipher (no client-side partition, noSdkRecordMapper.shouldIncludefilter, no deprecatedCipher.decrypt) and stampspartial: booleanon the decryptedCipherView/CipherListView.Web-vault-only, excluded-by-default streams.
CipherServicesplits its decrypted streams so partials are excluded by default and only the web list opts in:getAllDecrypted()excludes partials at the root (a privategetAllDecryptedIncludingPartials()retains them solely as the shared decrypt source), covering every imperative consumer — export, reports, Fido2, ssh-agent, autofill card/identity suggestions, key rotation, CLI.cipherViews$/cipherListViews$exclude partials, derived from private partials-inclusive sources so decryption stays single-pass.cipherListViewsWithPartials$.CipherViewLikeUtils.isPartialcentralizes the flag read.Read-only rendering + badge seams. A gated cipher opens read-only in the vault-item dialog (Edit hidden, form-mode redirected to view) and cipher-view, where a host can render an optional banner through the
CIPHER_VIEW_BANNERseam (handed theCipherViewdirectly). A host-provided "Controlled access" badge column (VAULT_ROW_LEASE_BADGEseam) appears only when thePamfeature flag is on, a PAM-enabled org (Organization.usePam) is in view, and a host provides the badge — otherwise the table is unchanged.Non-modifiable in web. Gated rows are non-selectable (checkbox disabled + excluded from
editableItems/select-all) and expose no modify menu actions; both bulk-action paths (classic emitters +VaultBatchBarService) filter them defensively; encrypted export excludes them via thepartialDatamarker.Filter tree. The web filter (type filter + folder tree, via a web-only
VaultFilterServicesubclass) includes partials so a type/folder present only as gated rows still surfaces; other clients keep the excluded stream. Collections are unaffected.Verification
test:types,lint, and prettier are clean on the affected projects (save the three SDK-blocked lines above). Touched + model specs pass, including new coverage for the stream split/exclusion,isPartial, the read-only rendering, the badge column gate, non-selectability, bulk-action + export exclusion, and key-rotation exclusion. E2E stays a no-op until a real PAM provider binds the badge seam and the server emitspartialData.🚨 Breaking Changes
None — inert until a PAM provider binds the badge seam and the server emits
partialData. With PAM off, no cipher ispartial, so every added filter is a no-op and behavior is unchanged.